"""cnn网络在mnist分类中的应用."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import numpy as np
import tensorflow as tf

from privacy.analysis import privacy_ledger
from privacy.analysis.rdp_accountant import compute_rdp_from_ledger
from privacy.analysis.rdp_accountant import get_privacy_spent
from privacy.optimizers import dp_optimizer

# Compatibility with tf 1 and 2 APIs
try:
  GradientDescentOptimizer = tf.train.GradientDescentOptimizer
  #GradientDescentOptimizer = tf.train.AdadeltaOptimizer
except:  # pylint: disable=bare-except
  GradientDescentOptimizer = tf.optimizers.SGD  # pylint: disable=invalid-name

#设置参数
tf.flags.DEFINE_boolean('dpsgd', True, 'If True, train with DP-SGD. If False, '
                        'train with vanilla SGD.')          #设置使用标准的sgd或者修改后的sgd
tf.flags.DEFINE_float('learning_rate', .15, 'Learning rate for training')
tf.flags.DEFINE_float('noise_multiplier', 1.0,
                      'Ratio of the standard deviation to the clipping norm')
tf.flags.DEFINE_float('l2_norm_clip', 1.0, 'Clipping norm')
tf.flags.DEFINE_integer('batch_size', 256, 'Batch size')  #训练数据的batchsize
tf.flags.DEFINE_integer('epochs', 70, 'Number of epochs')
tf.flags.DEFINE_integer('microbatches', 256, 'Number of microbatches '
                        '(must evenly divide batch_size)')
tf.flags.DEFINE_string('model_dir', None, 'Model directory')  #模型保存位置

FLAGS = tf.flags.FLAGS


class EpsilonPrintingTrainingHook(tf.train.SessionRunHook):
  """Training hook to print current value of epsilon after an epoch."""

  def __init__(self, ledger):
    """Initalizes the EpsilonPrintingTrainingHook.

    Args:
      ledger: The privacy ledger.
    """
    self._samples, self._queries = ledger.get_unformatted_ledger()

  def end(self, session):
    orders = [1 + x / 10.0 for x in range(1, 100)] + list(range(12, 64))
    samples = session.run(self._samples)
    queries = session.run(self._queries)
    formatted_ledger = privacy_ledger.format_ledger(samples, queries)
    rdp = compute_rdp_from_ledger(formatted_ledger, orders)
    eps = get_privacy_spent(orders, rdp, target_delta=1e-5)[0]
    print('For delta=1e-5, the current epsilon is: %.2f' % eps)


#cnn模型搭建
def cnn_model_fn(features, labels, mode):
  """Model function for a CNN."""
  '''用于实验的cnn模型'''

  # Define CNN architecture using tf.keras.layers.
  input_layer = tf.reshape(features['x'], [-1, 28, 28, 1])
  y = tf.keras.layers.Conv2D(16, 8,
                             strides=2,
                             padding='same',
                             activation='relu').apply(input_layer)
  y = tf.keras.layers.MaxPool2D(2, 1).apply(y)
  y = tf.keras.layers.Conv2D(32, 4,
                             strides=2,
                             padding='valid',
                             activation='relu').apply(y)
  y = tf.keras.layers.MaxPool2D(2, 1).apply(y)
  y = tf.keras.layers.Flatten().apply(y)
  y = tf.keras.layers.Dense(32, activation='relu').apply(y)
  logits = tf.keras.layers.Dense(10).apply(y)

  # Calculate loss as a vector (to support microbatches in DP-SGD).
  vector_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
      labels=labels, logits=logits)
  # Define mean of loss across minibatch (for reporting through tf.Estimator).
  scalar_loss = tf.reduce_mean(vector_loss)

  # Configure the training op (for TRAIN mode).
  if mode == tf.estimator.ModeKeys.TRAIN:

    if FLAGS.dpsgd:  #使用修改后的随机梯度下降算法
      print('\n\n\n\ 使用修改的optimizer \n\n\n')
      ledger = privacy_ledger.PrivacyLedger(
          population_size=60000, #样本总数每6000
          selection_probability=(FLAGS.batch_size / 60000),#指定每条样本包含在数据中的概率
          max_samples=1e6,
          max_queries=1e6)

      # 优化器的实例化，调用修改后的优化器，可以调用多种不同优化器
      #optimizer = dp_optimizer.DPGradientDescentGaussianOptimizer(
      optimizer = dp_optimizer.DPAdadeltaGaussianOptimizer(
          l2_norm_clip=FLAGS.l2_norm_clip,
          noise_multiplier=FLAGS.noise_multiplier,
          num_microbatches=FLAGS.microbatches,
          ledger=ledger,
          learning_rate=FLAGS.learning_rate)
      training_hooks = [
          EpsilonPrintingTrainingHook(ledger)
      ]
      opt_loss = vector_loss
    else:
      #optimizer = GradientDescentOptimizer(learning_rate=FLAGS.learning_rate)  #调用官方的优化
      optimizer = tf.train.AdadeltaOptimizer(learning_rate=FLAGS.learning_rate)
      print('\n\n 使用标准optimizer \n\n')
      training_hooks = []
      opt_loss = scalar_loss
    global_step = tf.train.get_global_step()
    train_op = optimizer.minimize(loss=opt_loss, global_step=global_step)
    # In the following, we pass the mean of the loss (scalar_loss) rather than
    # the vector_loss because tf.estimator requires a scalar loss. This is only
    # used for evaluation and debugging by tf.estimator. The actual loss being
    # minimized is opt_loss defined above and passed to optimizer.minimize().
    return tf.estimator.EstimatorSpec(mode=mode,
                                      loss=scalar_loss,
                                      train_op=train_op,
                                      training_hooks=training_hooks)

  # Add evaluation metrics (for EVAL mode).
  elif mode == tf.estimator.ModeKeys.EVAL:
    eval_metric_ops = {
        'accuracy':
            tf.metrics.accuracy(
                labels=labels,
                predictions=tf.argmax(input=logits, axis=1))
    }

    return tf.estimator.EstimatorSpec(mode=mode,
                                      loss=scalar_loss,
                                      eval_metric_ops=eval_metric_ops)


def load_mnist():#装载mnist数据集
  """Loads MNIST and preprocesses to combine training and validation data."""
  train, test = tf.keras.datasets.mnist.load_data()
  train_data, train_labels = train
  test_data, test_labels = test

  train_data = np.array(train_data, dtype=np.float32) / 255
  test_data = np.array(test_data, dtype=np.float32) / 255

  train_labels = np.array(train_labels, dtype=np.int32)
  test_labels = np.array(test_labels, dtype=np.int32)

  assert train_data.min() == 0.
  assert train_data.max() == 1.
  assert test_data.min() == 0.
  assert test_data.max() == 1.
  assert train_labels.ndim == 1
  assert test_labels.ndim == 1

  return train_data, train_labels, test_data, test_labels


def main(unused_argv):
  tf.logging.set_verbosity(tf.logging.INFO)
  if FLAGS.batch_size % FLAGS.microbatches != 0:
    raise ValueError('Number of microbatches should divide evenly batch_size')

  # Load training and test data.
  train_data, train_labels, test_data, test_labels = load_mnist()


  session_config = tf.ConfigProto(log_device_placement=False)
  # session_config.gpu_options.per_process_gpu_memory_fraction = 0.5
  run_config = tf.estimator.RunConfig().replace(session_config=session_config)

  # Instantiate the tf.Estimator.模型保存位置
  mnist_classifier = tf.estimator.Estimator(model_fn=cnn_model_fn,
                                            config=run_config,
                                            model_dir='./checkpoint')

  # Create tf.Estimator input functions for the training and test data.
  #输入用于训练的数据
  train_input_fn = tf.estimator.inputs.numpy_input_fn(
      x={'x': train_data},
      y=train_labels,
      batch_size=FLAGS.batch_size,
      num_epochs=FLAGS.epochs,
      shuffle=True)
  #输入测试数据集
  eval_input_fn = tf.estimator.inputs.numpy_input_fn(
      x={'x': test_data},
      y=test_labels,
      num_epochs=1,
      shuffle=False)

  # 循环训练
  #每个epoch训练的步数
  #训练234次
  steps_per_epoch = 60000 // FLAGS.batch_size
  w=open('./result/高斯1.0,l2=1.0.txt','w')
  for epoch in range(1, FLAGS.epochs + 1):
    # 训练模型
    #tensors_to_log = {"probabilities": "softmax_tensor"}
    #logging_hook = tf.train.LoggingTensorHook(tensors=tensors_to_log, every_n_iter=50)
    mnist_classifier.train(input_fn=train_input_fn, steps=steps_per_epoch)

    # Evaluate the model and print results
    #每训练完一次后测试准确率
    eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
    test_accuracy = eval_results['accuracy']
    print('Test accuracy after %d epochs is: %.3f' % (epoch, test_accuracy)) #打印测试的准确率
    s = str(epoch) + ',' + str(test_accuracy)+'\n'
    w.write(s)
  w.close()

if __name__ == '__main__':
  tf.app.run()
